package com.news.cloud.service;

import com.news.cloud.exception.ServerTechnicalException;
import com.news.cloud.exception.UnauthorizedException;
import com.news.cloud.jpa.domain.Channel;
import com.news.cloud.jpa.domain.Feed;
import com.news.cloud.jpa.domain.Provider;
import com.news.cloud.jpa.domain.User;
import com.news.cloud.jpa.repository.FeedRepository;
import com.news.cloud.web.model.FeedPageResponse;
import com.news.cloud.web.model.FeedResponse;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.OptionalDouble;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.LongStream;

/**
 * Created by hiephn on 2014/08/28.
 */
@Service
public class FeedService extends AbstractService {

    @Autowired
    private FeedRepository feedRepo;
    @Autowired
    private SecurityService securityService;

    private SimpleDateFormat feedDateFormat = new SimpleDateFormat("EEE, d MMM yyyy hh:mm:ss Z");
    private Pattern imgPattern = Pattern.compile("http://[^<]*((\\.jp[a-z]*)|(\\.png))");
    private static final int FEED_PAGE_SIZE = 30;
    private static final int HOT_WEIGHT = 10;
    private static final int HOBBY_WEIGHT = 10;
    private static final int FIRST_PAGE_WEIGHT = 5;
    private static final int READ_WEIGHT = -2;
    private static final int NEW_WEIGHT = 10;

    public FeedPageResponse getAll() throws ServerTechnicalException {
        Page<Feed> feeds = feedRepo.findAll(new PageRequest(0, 1000));
        FeedPageResponse feedPageResponse = new FeedPageResponse();
        feedPageResponse.from(feeds, FeedResponse.class);
        return feedPageResponse;
    }

    /**
     * Get News feed page:
     *
     * @param page
     * @return
     */
    public FeedPageResponse get(int page) throws UnauthorizedException, ServerTechnicalException {

        // Get current user
        List<Channel> userChannels = new ArrayList<>();
        User user = null;
        if (securityService.isUserLogin()) {
            user = securityService.retrieveCurrentUser();
            user.getHobbies().forEach(hobby -> userChannels.add(hobby.getChannel()));
        }

        // List #1
        // get feeds that are on provider first page (homepage)
        List<Integer> firstPageFeedIds = feedRepo.
                findFirstPageIdsOrderByPubDateDesc(FEED_PAGE_SIZE / 3, (FEED_PAGE_SIZE / 3) * page);

        // List #2
        // get user's hobby feeds
        PageRequest hobbyPageRequest = new PageRequest(page, FEED_PAGE_SIZE/3, Sort.Direction.DESC, "pubDate");
        Page<Feed> hobbyPage = feedRepo.findByChannelIn(userChannels, hobbyPageRequest);
        OptionalDouble doubleStream = hobbyPage.getContent().stream().mapToDouble(Feed::getHot).max();
        int hobbyMaxHot = doubleStream.isPresent()? (int)doubleStream.getAsDouble(): 0;
        doubleStream = hobbyPage.getContent().stream().mapToDouble(Feed::getHot).min();
        int hobbyMinHot = doubleStream.isPresent()? (int)doubleStream.getAsDouble(): 0;

        // List #3
        // get hot feeds not in user's hobby channels
        PageRequest hotPageRequest = new PageRequest(page, FEED_PAGE_SIZE/3);
        Page<Feed> hotPage = feedRepo.findByChannelNotInOrderByHotDesc(userChannels, hotPageRequest);
        final int maxHot = Math.max(hobbyMaxHot, (int)hotPage.getContent().stream().mapToDouble(Feed::getHot).max().getAsDouble());
        final int minHot = Math.min(hobbyMinHot, (int)hotPage.getContent().stream().mapToDouble(Feed::getHot).min().getAsDouble());

        // List #4 will contains all the output feeds
        List<Feed> feeds = new ArrayList<>(FEED_PAGE_SIZE); // with max size

        /**
         * 1) Hot & Hobby weighting (1) for List #1 & #2
         *
         * algorithm:
         *   isHot? +10
         *   isHobby? +10
         *   isFirstPage? +5
         *
         * 2) Remove duplicated feeds from List #1 (List #2 & #3 will never be duplicated)
         *
         * 3) Merge 3 lists into List #4
         */
        List<Integer> firstPageDupIds = new ArrayList<>(firstPageFeedIds.size());
        hobbyPage.forEach(feed -> {
            int hotWeight = (((maxHot - minHot) > 0) ?
                    (feed.getHot() - minHot) * HOT_WEIGHT / (maxHot - minHot) : 0);
            int firstPageWeight = 0;
            if (firstPageFeedIds.contains(feed.getId())) {
                firstPageDupIds.add(feed.getId()); // get duplication ids
                firstPageWeight = FIRST_PAGE_WEIGHT;
            }
            feed.setWeight(feed.getWeight() +
                HOBBY_WEIGHT + // hobby weight
                hotWeight + // hot weight
                firstPageWeight); // first page weight
            feeds.add(feed); // add into List #4
        });
        hotPage.forEach(feed -> {
            int firstPageWeight = 0;
            if (firstPageFeedIds.contains(feed.getId())) {
                firstPageDupIds.add(feed.getId()); // get duplication ids
                firstPageWeight = FIRST_PAGE_WEIGHT;
            }
            feed.setWeight(feed.getWeight() +
                HOT_WEIGHT + // hot weight
                firstPageWeight); // first page weight
            feeds.add(feed); // add into List #4
        });
        // remove from first page list ids that are already contained in the other lists
        firstPageFeedIds.removeAll(firstPageDupIds);
        // get the first page entities
        List<Feed> firstPageFeeds = feedRepo.findByIdIn(firstPageFeedIds);
        firstPageFeeds.forEach(feed -> {
            feed.setWeight(feed.getWeight() + FIRST_PAGE_WEIGHT); // first page weight
            feeds.add(feed); // add into List #4
        });

        // join into 1 list
//        hotPage.forEach(feeds::add);
//        hobbyPage.forEach(feeds::add);
//        firstPageFeeds.forEach(feeds::add);

        // get feed ids that user already read
        List<Integer> readFeedIds = null;
        List<Integer> feedIds = feeds.stream()
                .map(feed -> feed.getId())
                .collect(java.util.stream.Collectors.toList());
        if (securityService.isUserLogin()) {
            readFeedIds = feedRepo.findReadIds(user.getId(), feedIds);
        }

        /**
         * weighting (2) and sorting
         *
         * algorithm:
         *   isReadOnce? -2
         *   isNewest? +10
         *   isOldest? +0
         *   isFirstPage? +5
         *
          */
        final long newest = feeds.stream().mapToLong(feed -> feed.getLastUpdate().getTime()).max().getAsLong();
        final long oldest = feeds.stream().mapToLong(feed -> feed.getLastUpdate().getTime()).min().getAsLong();
        for (Feed feed : feeds) {
            feed.setWeight(feed.getWeight() +
                    (((newest - oldest) > 0) ?
                        (int)((feed.getLastUpdate().getTime() - oldest) * NEW_WEIGHT / (newest - oldest)) :
                        0));
            if (readFeedIds != null) {
                if (readFeedIds.contains(feed.getId())) {
                    feed.setWeight(feed.getWeight() + READ_WEIGHT);
                }
            }
        };
        feeds.sort((a, b) -> b.getWeight() - a.getWeight());

        // make response data
        FeedPageResponse feedPageResponse = new FeedPageResponse();
        feedPageResponse.from(feeds, FeedResponse.class);

        return feedPageResponse;
    }

    @Transactional
    public Feed createWithUpdate(String name, String desc, String link, String pubDate, Channel channel,
                                 Provider provider) throws ParseException {
        Date date = this.feedDateFormat.parse(pubDate);
        Timestamp timestamp = new Timestamp(date.getTime());
        String providerFeedId = this.extractProviderFeedId(link);

        Feed feed = feedRepo.findOneByProviderFeedIdAndProviderId(providerFeedId, provider.getId());
        if (feed == null) {
            feed = new Feed();
            feed.setPubDate(timestamp);
        }

        feed.setName(name);
        feed.setChannel(channel);
        feed.setProvider(provider);
        feed.setName(name);
        feed.setDesc(StringUtils.substring(
                desc,
                StringUtils.lastIndexOf(desc, '>') + 1));
        feed.setLink(link);
        feed.setLastUpdate(timestamp);
        feed.setProviderFeedId(providerFeedId);

        Matcher matcher = imgPattern.matcher(desc);
        if (matcher.find()) {
            String imgUrl = matcher.group(0);
//            imgUrl = StringUtils.replace(imgUrl, "_180x108", "");
            feed.setImgUrl(imgUrl);
        }
        feedRepo.save(feed);
        return feed;
    }

    /**
     * To know if a feed is on provider's first page (home page),
     * we need to know provider feed Id and provider Id
     * @param link from which provider feed Id is extracted
     * @param providerId provider Id
     */
    @Transactional
    public void makeFirstPage(String link, int providerId, String pubDate) throws ParseException {
        Date date = this.feedDateFormat.parse(pubDate);
        Timestamp timestamp = new Timestamp(date.getTime());
        String providerFeedId = this.extractProviderFeedId(link);
        Feed feed = feedRepo.findOneByProviderFeedIdAndProviderId(providerFeedId, providerId);
        if (feed != null) {
            feedRepo.saveFirstPageFeed(providerId, feed.getId(), timestamp);
        }
    }

    @Transactional
    public void resetFirstPageFeed(int providerId) {
        feedRepo.resetFirstPageFeed(providerId);
    }

    protected String extractProviderFeedId(String link) {
        return StringUtils.substring(
                link,
                StringUtils.lastIndexOf(link, '-') + 1,
                StringUtils.lastIndexOf(link, ".html")
        );
    }

    public static void main(String[] args) {
        List<Long> ids = Arrays.asList(1l, 2l, 5l, 9l, 4l);
        LongStream stream = ids.stream().mapToLong(a -> a);
        System.out.println(stream.max());
        System.out.println(stream.min());
    }
}
